﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using DistributedGraph;

namespace CAP.Forms
{
    /// <summary>
    /// Control-Klasse zum Darstellen des Graphen.
    /// </summary>
    public partial class NodeGraph : System.Windows.Forms.PictureBox
    {
        #region Konstanten
        private const int FPS = 16;
        #endregion Konstanten

        #region Typen
        public delegate void NodeSelectedEvent(object sender, Node n);
        public delegate void DrawNodeEvent(Node node, ExtendedDrawingInformation edi);

        public class ExtendedDrawingInformation
        {
            #region RightTop
            /// <summary>
            /// Der normale Text. (rechts Oben)
            /// </summary>
            public string TextRightTop = "";

            /// <summary>
            /// Der hochgestellte Text. (rechts Oben)
            /// </summary>
            public string TextRightTopHigh = "";

            /// <summary>
            /// Der tiefgestellte Text. (rechts Oben)
            /// </summary>
            public string TextRightTopLow = "";

            /// <summary>
            /// Die Schriftart des Textes. (rechts Oben)
            /// </summary>
            public string TextRightTopFont = "Verdana";

            /// <summary>
            /// Die Farbe des Textes. (rechts Oben)
            /// </summary>
            public Color ColorRightTop = Color.Black;
            #endregion RightTop

            #region RightBottom
            /// <summary>
            /// Der normale Text. (rechts Unten)
            /// </summary>
            public string TextRightBottom = "";

            /// <summary>
            /// Der hochgestellte Text. (rechts Unten)
            /// </summary>
            public string TextRightBottomLow = "";

            /// <summary>
            /// Der tiefgestellte Text. (rechts Unten)
            /// </summary>
            public string TextRightBottomHigh = "";

            /// <summary>
            /// Die Schriftart des Textes. (rechts Unten)
            /// </summary>
            public string TextRightBottomFont = "Verdana";

            /// <summary>
            /// Die Farbe des Textes. (rechts Unten)
            /// </summary>
            public Color ColorRightBottom = Color.Black;
            #endregion RightBottom
        }
        #endregion Typen

        #region Attribute
        private Node[] nodes;
        private Timer animate;
        private List<NodeGraphAnimation> animations = new List<NodeGraphAnimation>();
        private object animateSyncLock = new object();
        private Node selectedNode;
        private Dictionary<Node, Point> positioning = new Dictionary<Node, Point>();
        #endregion Attribute

        #region Eigenschaften#region Eigenschaften
        /// <summary>
        /// Setzt und gibt die angezeigte Reihung von Knoten.
        /// </summary>
        public Node[] Nodes
        {
            get { return nodes; }
            set
            {
                nodes = value;
                SelectedNode = null;
                if (nodes == null) return;
                foreach (Node node in value) node.MsgSent += new Node.SimpleMsgEvent(node_MsgSent);
                Invalidate();
            }
        }

        /// <summary>
        /// Gibt oder setzt den markierten Knoten.
        /// </summary>
        public Node SelectedNode
        {
            get { return selectedNode; }
            set
            {
                selectedNode = value;
                if (NodeSelected != null) NodeSelected(new object(), selectedNode);
            }
        }

        /// <summary>
        /// Wird gerufen sobald der markierte Knoten gewechselt wurde.
        /// </summary>
        public event NodeSelectedEvent NodeSelected;

        /// <summary>
        /// Wird gerufen sobald die Grafik gezeichnet wurde.
        /// </summary>
        public event DrawNodeEvent ExtendNode;
        #endregion Eigenschaften

        #region Methoden
        /// <summary>
        /// Initiiert und intialisert die Klasse.
        /// </summary>
        public NodeGraph()
        {
            InitializeComponent();
            base.BackColor = Color.White;
            animate = new Timer();

            animate.Tick += new EventHandler(animate_Tick);
            animate.Interval = 1000 / FPS;
            animate.Enabled = true;
        }

        /// <summary>
        /// Animationsengine Runtime.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void animate_Tick(object sender, EventArgs e)
        {
            lock (animateSyncLock)
            {
                List<NodeGraphAnimation> elapsedAnimations = new List<NodeGraphAnimation>();
                foreach (NodeGraphAnimation nga in animations) if (nga.IsDone) elapsedAnimations.Add(nga);
                foreach (NodeGraphAnimation nga in elapsedAnimations) animations.Remove(nga);
                if (elapsedAnimations.Count > 0 || animations.Count > 0) Invalidate(); // Um Rechenzeit zu sparen.
            }
        }


        /// <summary>
        /// Setzt die Kantenanimation wenn etwas gesendet wurde.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="receiver"></param>
        /// <param name="link"></param>
        /// <param name="message"></param>
        void node_MsgSent(Node sender, Node receiver, Link link, IMessage message)
        {
            lock (animateSyncLock)
            {
                animations.Add(new NodeGraphAnimationLink(2000, link, sender, receiver));
            }
        }

        /// <summary>
        /// Gibt die zugehörige Animation zu einer spezifischen Kante an.
        /// </summary>
        /// <param name="link">Die spezifische Kante.</param>
        /// <returns>Die Animation der Kante. (Null wenn nicht vorhanden)</returns>
        private NodeGraphAnimationLink GetLinkAnimation(Link link)
        {
            int i = 0;
            while (i < animations.Count && !(animations[i] is NodeGraphAnimationLink && ((NodeGraphAnimationLink)animations[i]).Link == link)) i++;
            return i < animations.Count ? ((NodeGraphAnimationLink)animations[i]) : null;
        }

        /// <summary>
        /// Führt die eigentliche Zeichnung des Graphen durch.
        /// </summary>
        /// <param name="pe"></param>
        protected override void OnPaint(PaintEventArgs pe)
        {
            base.OnPaint(pe);

            if (nodes == null || nodes.Length == 0) return;

            int diameter = (int)(Math.Min(Height, Width) * 0.9f);
            int radius = diameter / 2;
            float angleBetweenNodes = 360.0f / nodes.Length;

            positioning.Clear();
            for (int i = 0; i < nodes.Length; i++) { positioning.Add(nodes[i], calcPosition(new Point(Width / 2, Height / 2), radius, i * angleBetweenNodes)); }

            pe.Graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
            pe.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
            pe.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

            lock (animateSyncLock)
            {
                // Zeichne Kanten da die Knoten diese überdecken
                foreach (KeyValuePair<Node, Point> kvp in positioning)
                {
                    foreach (Link l in kvp.Key.Links)
                    {
                        // TryCatch um den 0 zu 0 Fall abzufangen. (Prüfung zu kostspielig)
                        try
                        {
                            NodeGraphAnimationLink ngal = GetLinkAnimation(l);
                            if (ngal == null)
                            {
                                pe.Graphics.DrawLine(Pens.Black, positioning[l.SourceNode], positioning[l.TargetNode]);
                            }
                            else
                            {
                                Color originColor = Color.Blue;
                                Color targetColor = Color.Blue;
                                if (ngal.Progress < 0.25)
                                {
                                    originColor = Color.FromArgb((int)(255 * ngal.Progress * 4),
                                                            (int)(255 * ngal.Progress * 4),
                                                            (int)(255 * ngal.Progress * 4));
                                    targetColor = Color.FromArgb(0, 0, 0);
                                }
                                else if (ngal.Progress < 0.5)
                                {
                                    originColor = Color.FromArgb(255, 255, 255);
                                    targetColor = Color.FromArgb((int)(255 * (ngal.Progress - 0.25) * 4),
                                                            (int)(255 * (ngal.Progress - 0.25) * 4),
                                                            (int)(255 * (ngal.Progress - 0.25) * 4));
                                }
                                else if (ngal.Progress < 0.75)
                                {
                                    originColor = Color.FromArgb((int)(255 - 255 * (ngal.Progress - 0.5) * 4),
                                                            (int)(255 - 255 * (ngal.Progress - 0.5) * 4),
                                                            (int)(255 - 255 * (ngal.Progress - 0.5) * 4));
                                    targetColor = Color.FromArgb(255, 255, 255);

                                }
                                else
                                {
                                    originColor = Color.FromArgb(0, 0, 0);
                                    targetColor = Color.FromArgb((int)(255 - 255 * (ngal.Progress - 0.75) * 4),
                                                            (int)(255 - 255 * (ngal.Progress - 0.75) * 4),
                                                            (int)(255 - 255 * (ngal.Progress - 0.75) * 4));
                                }

                                Pen pen = new Pen(new System.Drawing.Drawing2D.LinearGradientBrush(positioning[ngal.From], positioning[ngal.To], originColor, targetColor), 2);
                                pe.Graphics.DrawLine(pen, positioning[ngal.From], positioning[ngal.To]);

                                Mathematics.Vector vt = new Mathematics.Vector(positioning[ngal.From], positioning[ngal.To]);
                                vt.FitToLength((float)(vt.GetAbsoluteValue() * ngal.Progress));
                                Point targetPos = vt.ToPoint(positioning[ngal.From]);
                                Mathematics.Vector vo = new Mathematics.Vector(targetPos, positioning[ngal.From]);
                                vo.FitToLength((float)((new Mathematics.Vector(positioning[ngal.From], positioning[ngal.To])).GetAbsoluteValue() * 0.05));
                                Point originPos = vo.ToPoint(targetPos);

                                Pen boldPen = new Pen(Color.Red, 4);
                                pe.Graphics.DrawLine(boldPen, originPos, targetPos);
                            }
                        }
                        catch { }
                    }
                }

                // Zeichne Knoten
                int nodeSize = radius / 5;
                foreach (KeyValuePair<Node, Point> kvp in positioning)
                {
                    pe.Graphics.FillEllipse(kvp.Key.FailStops ? (kvp.Key == selectedNode ? Brushes.DarkViolet : Brushes.DarkRed) : (kvp.Key == selectedNode ? Brushes.DarkBlue : Brushes.Black), kvp.Value.X - nodeSize / 2, kvp.Value.Y - nodeSize / 2, nodeSize, nodeSize);

                    Pen dotPen = new Pen(Color.Blue, 2.5f);
                    dotPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
                    if (kvp.Key == selectedNode) pe.Graphics.DrawEllipse(new Pen(Color.White, nodeSize / 20), kvp.Value.X - nodeSize / 2 + (nodeSize / 30), kvp.Value.Y - nodeSize / 2 + (nodeSize / 30), nodeSize - (nodeSize / 15), nodeSize - (nodeSize / 15));
                    if (kvp.Key == selectedNode) pe.Graphics.DrawEllipse(dotPen, kvp.Value.X - nodeSize / 2, kvp.Value.Y - nodeSize / 2, nodeSize, nodeSize);
                    pe.Graphics.DrawString(kvp.Key.Id.ToString(), new Font("Verdana", Math.Max(1, nodeSize / 4)), Brushes.White, new PointF(kvp.Value.X - nodeSize / 8 - (kvp.Key.Id.ToString().Length / 2) * (nodeSize / 5), kvp.Value.Y - nodeSize / 4));

                    ExtendedDrawingInformation edi = new ExtendedDrawingInformation();
                    if (ExtendNode != null) ExtendNode(kvp.Key, edi);

                    if (edi.TextRightTop.Length > 0)
                    {
                        Pen pen = new Pen(edi.ColorRightTop);
                        pe.Graphics.DrawString(edi.TextRightTop, new Font(edi.TextRightTopFont, Math.Max(1, nodeSize / 4.5f)), pen.Brush, new PointF(kvp.Value.X + nodeSize / 2, kvp.Value.Y - nodeSize / 4 - nodeSize / 3));

                        // Hoch- und Tiefgestellte Zeilen
                        pe.Graphics.DrawString(edi.TextRightTopHigh, new Font(edi.TextRightTopFont, Math.Max(1, nodeSize / 4.5f / 4 * 3)), pen.Brush, new PointF(kvp.Value.X + nodeSize / 2, kvp.Value.Y - nodeSize / 4 - nodeSize / 3 - (Math.Max(1, nodeSize / 4.5f / 6 * 3))));
                        pe.Graphics.DrawString(edi.TextRightTopLow, new Font(edi.TextRightTopFont, Math.Max(1, nodeSize / 4.5f / 4 * 3)), pen.Brush, new PointF(kvp.Value.X + nodeSize / 2, kvp.Value.Y - nodeSize / 4 - nodeSize / 3 + (Math.Max(1, nodeSize / 4.5f / 4 * 3))));
                    }
                    if (edi.TextRightBottom.Length > 0)
                    {
                        Pen pen = new Pen(edi.ColorRightBottom);
                        pe.Graphics.DrawString(edi.TextRightBottom, new Font(edi.TextRightBottomFont, Math.Max(1, nodeSize / 4.5f)), pen.Brush, new PointF(kvp.Value.X + nodeSize / 2, kvp.Value.Y - nodeSize / 4 + nodeSize / 2.5f));

                        // Hoch- und Tiefgestellte Zeilen
                        pe.Graphics.DrawString(edi.TextRightBottomHigh, new Font(edi.TextRightBottomFont, Math.Max(1, nodeSize / 4.5f / 4 * 3)), pen.Brush, new PointF(kvp.Value.X + nodeSize / 2, kvp.Value.Y - nodeSize / 4 - nodeSize / 3 - (Math.Max(1, nodeSize / 4.5f / 6 * 3))));
                        pe.Graphics.DrawString(edi.TextRightBottomLow, new Font(edi.TextRightBottomFont, Math.Max(1, nodeSize / 4.5f / 4 * 3)), pen.Brush, new PointF(kvp.Value.X + nodeSize / 2, kvp.Value.Y - nodeSize / 4 - nodeSize / 3 + (Math.Max(1, nodeSize / 4.5f / 4 * 3))));
                    }
                }
            }
        }

        /// <summary>
        /// Wird bei einer regulären Größenänderung ausgeführt.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnResize(EventArgs e)
        {
            Invalidate();
            base.OnResize(e);
        }

        /// <summary>
        /// Berechnet die Position eines Knoten.
        /// </summary>
        /// <param name="center">Mittelpunkt von dem die Position ausgehen soll.</param>
        /// <param name="radius">Der Radius der Knoten.</param>
        /// <param name="angle">Der Winkel des spezifischen Knoten.</param>
        /// <returns>Die errechnete Position.</returns>
        private Point calcPosition(Point center, int radius, float angle)
        {
            Mathematics.Vector target = new Mathematics.Vector(angle);
            target.FitToLength(radius);
            PointF pos = target.ToPoint(new PointF(0, 0));
            return new Point(center.X - ((int)pos.X), center.Y - ((int)pos.Y));
        }

        /// <summary>
        /// Markiert einen Knoten falls er markiert wurde.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void NodeGraph_MouseClick(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                int diameter = (int)(Math.Min(Height, Width) * 0.9f);
                int radius = diameter / 2;
                int nodeSize = radius / 5;

                int i = 0;
                while (i < nodes.Length && !(
                    positioning[nodes[i]].X - nodeSize / 2 <= e.X && positioning[nodes[i]].X + nodeSize / 2 >= e.X &&
                    positioning[nodes[i]].Y - nodeSize / 2 <= e.Y && positioning[nodes[i]].Y + nodeSize / 2 >= e.Y)) i++;
                selectedNode = i < nodes.Length ? nodes[i] : null;
                if (NodeSelected != null) NodeSelected(sender, selectedNode);

                Invalidate();
            }
            else if (e.Button == System.Windows.Forms.MouseButtons.Right)
            {
                selectedNode = null;
                if (NodeSelected != null) NodeSelected(sender, selectedNode);
                Invalidate();
            }
        }
        #endregion Methoden
    }
}
